/*
 * MatrixMultiplication.cpp
 *
 *  Created on: 09-Dec-2012
 *      Author: rohin
 */

#include<CL/opencl.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include"MatrixMultiplication.h"
#include"SelectOpenCLPlatform.h"

#define MAX_SOURCE_SIZE 100000

cl_context hContext;
cl_device_id cdDevice;
cl_program hProgram;
cl_command_queue hCmdQueue;
cl_kernel ckKernel;

cl_int selectPlatform(cl_platform_id* clSelectedPlatform){
	
	 char chBuffer[1024];
	cl_uint numPlatforms;
	cl_int ciErrNum = CL_SUCCESS;
	*clSelectedPlatform = NULL;
	cl_platform_id *platformIds;

	ciErrNum = clGetPlatformIDs (0, NULL, &numPlatforms);
	if (ciErrNum != CL_SUCCESS)
    {
        printf("Error: Failed to get number of OpenCL Platform!\n");
        return ciErrNum;
    }
	 else 
    {
        if(numPlatforms == 0)
        {
            printf("No OpenCL platform found!\n\n");
            return -2000;
        }
		 else 
        {
            // if there's a platform or more, make space for ID's
            if ((platformIds = (cl_platform_id*)malloc(numPlatforms * sizeof(cl_platform_id))) == NULL)
            {
                printf("Failed to allocate memory for cl_platform ID's!\n\n");
                return -3000;
            }

            // get platform info for each platform and trap the NVIDIA platform if found
            ciErrNum = clGetPlatformIDs (numPlatforms, platformIds, NULL);
            for(cl_uint i = 0; i < numPlatforms; ++i)
            {
                ciErrNum = clGetPlatformInfo (platformIds[i], CL_PLATFORM_NAME, 1024, &chBuffer, NULL);
				printf("\nPlatform: \n");
				printf(chBuffer);

                if(ciErrNum == CL_SUCCESS)
                {
                    if(strstr(chBuffer, "NVIDIA") != NULL)
                    {
                        *clSelectedPlatform = platformIds[i];
                        break;
                    }
					else if(strstr(chBuffer, "AMD") != NULL)
                    {
                        *clSelectedPlatform = platformIds[i];
                        break;
                    }
                }
            }

            // default to zeroeth platform if NVIDIA not found
            if(*clSelectedPlatform == NULL)
            {
                printf("WARNING: NVIDIA or AMD OpenCL platform not found - defaulting to first platform!\n\n");
                *clSelectedPlatform = platformIds[0];
            }

            free(platformIds);
        }

	}
	printf("\n\nNumber of OpenCL Platforms = %d \n\n", numPlatforms);

	//Get the PlatformIds
	platformIds = new cl_platform_id[numPlatforms];
	ciErrNum = clGetPlatformIDs(numPlatforms, platformIds, NULL);
	if (ciErrNum != CL_SUCCESS)
    {
        printf("Error: Failed to get OpenCL PlatformIds!\n");
        return ciErrNum;
    }

	return ciErrNum;
}

int InitializeOpenCL(char* fullFilePath, int filePathSize)
{
        cl_int ciErrNum = CL_SUCCESS;
        cl_platform_id clPlatform;

        //Get OpenCL Platform
        ciErrNum = selectPlatform (&clPlatform);

        //Get GPU device from the platform
        ciErrNum = clGetDeviceIDs(clPlatform, CL_DEVICE_TYPE_GPU, 1, &cdDevice, NULL);
        if (ciErrNum != CL_SUCCESS)
    {
        printf("Error: Failed to get GPU OpenCL Device!\n");
                printf("Checking for CPU Device!\n");

                ciErrNum = clGetDeviceIDs(clPlatform, CL_DEVICE_TYPE_CPU, 1, &cdDevice, NULL);
                if (ciErrNum != CL_SUCCESS)
                {
                printf("Error: Failed to get CPU OpenCL Device!\n");
                        return ciErrNum;
                }
        }

                char chDeviceName[256];
                clGetDeviceInfo(cdDevice, CL_DEVICE_NAME, 256, chDeviceName, NULL);

                printf("Device Name : ");
                printf(chDeviceName);
                printf("\n\n");

        //Create Context
        hContext = clCreateContext ( 0, 1, &cdDevice, NULL, NULL, NULL);

        //Set the kernel file path
        FILE *fp;
        char *fileName;
        fileName = (char*)malloc(filePathSize);
        fileName = fullFilePath;

        char *source_str;
        size_t source_size;

        /* Load the source code containing the kernel*/
        fp = fopen(fileName, "r");
        if (!fp) {
                printf("Failed to load kernel.\n");
                exit(1);
        }

        source_str = (char*)malloc(MAX_SOURCE_SIZE);
        source_size = fread( source_str, 1, MAX_SOURCE_SIZE, fp);
        fclose( fp );

        //Create and compile program
        hProgram = clCreateProgramWithSource(hContext, 1, (const char **)&source_str, (const size_t *)&source_size, &ciErrNum);

        if (ciErrNum != CL_SUCCESS)
    {
        printf("Error: Failed to create program\n");
        return ciErrNum;
    }
    free(source_str);

        /* Build Kernel Program */
        ciErrNum = clBuildProgram(hProgram, 0, NULL, NULL, NULL, NULL);
        if (ciErrNum != CL_SUCCESS)
    {
        printf("Error: Failed to build program\n");
        size_t length;
                char buffer[2048];
                clGetProgramBuildInfo(hProgram, cdDevice, CL_PROGRAM_BUILD_LOG, sizeof(buffer), buffer, &length);

                printf("Build Log: %s\n", buffer);
                getchar();
        return ciErrNum;
    }

        return ciErrNum;
}


int run_program(int *MatrixA, int *MatrixB, int *MatrixC, int a, int b, int c){
        cl_int ciErrNum = CL_SUCCESS;
        cl_int ciErr1, ciErr2;

int multiplier = Multiplier;
        char fileName[] = "MatrixMultiplication.cl";
        int filePathSize = 18;

        //Initialize the OpenCL device, Context and Program
        ciErrNum = InitializeOpenCL(fileName, filePathSize);
	if (ciErrNum != CL_SUCCESS)
    {
        printf("Error: Failed to Initialize OpenCL Program\n");
        return ciErrNum;
    }
        printf("Initialized OpenCL Program..\n");
				
	//Declare OpenCL Runtime variables
	cl_command_queue hCmdQueue;
	 
	//Create Command Queue
	hCmdQueue = clCreateCommandQueue(hContext, cdDevice, 0, &ciErrNum);
	if (ciErrNum != CL_SUCCESS)
	{
        printf("Error: Failed to Initialize OpenCL Program\n");
        return ciErrNum;
	}
	printf("Command queue created..\n");

	//Creating device buffers
	cl_mem bufferA;
	cl_mem bufferB;
	cl_mem bufferC;

	 bufferA = clCreateBuffer(hContext, CL_MEM_READ_WRITE, a * c * sizeof(int), NULL, &ciErr1);
	bufferB = clCreateBuffer(hContext, CL_MEM_READ_ONLY, c * b * sizeof(int), NULL, &ciErr2);
	ciErr1 |= ciErr2;
	bufferC = clCreateBuffer(hContext, CL_MEM_WRITE_ONLY, a * b * sizeof(int), NULL, &ciErr2);
	ciErr1 |= ciErr2;
	if (ciErr1 != CL_SUCCESS)
	{
        printf("Error: Failed to Create Buffer: %d\n", ciErr1);
	}
   
	//writing data to device buffers
	ciErrNum = clEnqueueWriteBuffer(hCmdQueue, bufferA, CL_FALSE, 0, a * c * sizeof(int), MatrixA, 0, NULL, NULL);
	ciErrNum = clEnqueueWriteBuffer(hCmdQueue, bufferB, CL_FALSE, 0, c * b * sizeof(int), MatrixB, 0, NULL, NULL);
		if (ciErrNum != CL_SUCCESS)
	{
        printf("Error: Failed to Write Buffer: %d\n", ciErrNum);
	}
   printf("Data written to device buffers..\n");
	
	ckMultKernel = clCreateKernel(hProgram, "MatrixMultiplication_KERNEL", &ciErrNum);
	if (ciErrNum != CL_SUCCESS)
	{
        printf("Error: Failed to create ckMultKernel: %d\n", ciErrNum);
	}
	ckKernel = clCreateKernel(hProgram, "ScalarMatrix_KERNEL", &ciErrNum);
	if (ciErrNum != CL_SUCCESS)
	{
        printf("Error: Failed to create ckKernel: %d\n", ciErrNum);
	}
	printf("Created the kernel(s)..\n");

	size_t globalWorkSize[2];
	/*localWorkSize[0] = 8;
	localWorkSize[1] = 8;*/
	globalWorkSize[0] = a*b;
	globalWorkSize[1] = b;

	//size_t globalWorkSize[1];
	//globalWorkSize[0] = 1;

	printf("Setting the kernel args..\n");
	// Set the kernel arguments for ckMultKernel
	ciErrNum = clSetKernelArg(ckMultKernel, 0, sizeof(cl_mem), &bufferA);
	ciErrNum |= clSetKernelArg(ckMultKernel, 1, sizeof(cl_mem), &bufferB);
	ciErrNum |= clSetKernelArg(ckMultKernel, 2, sizeof(cl_mem), &bufferC);
	ciErrNum |= clSetKernelArg(ckMultKernel, 3, sizeof(int), &a);
	ciErrNum |= clSetKernelArg(ckMultKernel, 4, sizeof(int), &b);
	if(ciErrNum != CL_SUCCESS){
		printf("Error while setting kernel Arguments4 for ckMultKernel: %d\n", ciErrNum);
	}
   
   	// Set the kernel arguments for ckKernel
	ciErrNum = clSetKernelArg(ckKernel, 0, sizeof(cl_mem), &bufferA);
	ciErrNum |= clSetKernelArg(ckKernel, 1, sizeof(int), &c);
	ciErrNum |= clSetKernelArg(ckKernel, 2, sizeof(int), &multiplier);
	if(ciErrNum != CL_SUCCESS){
		printf("Error while setting kernel Arguments2 for ckKernel: %d\n", ciErrNum);
	}

	#pragma region Call_MatrixMultiplication
	printf("Enqueuing the Matrix Multiplication kernel..\n");
	ciErrNum = clEnqueueNDRangeKernel(hCmdQueue, ckMultKernel, 1, NULL, globalWorkSize, NULL, 0, NULL, NULL);
	if(ciErrNum != CL_SUCCESS){
		printf("Error while enqueuing ckMultKernel");
	}
	clFinish(hCmdQueue); 
	printf("Completed execution..\n\n");

	clEnqueueReadBuffer(hCmdQueue, bufferC, CL_TRUE, 0, a * b * sizeof(int), MatrixC, 0, NULL, NULL);
	if(ciErrNum != CL_SUCCESS){
		printf("Error while reading buffers: %d\n", ciErrNum);
	}
	printf("Results received..\n");
	#pragma endregion
   
   	#pragma region Call_Scalar_Matrix
	printf("\nEnqueuing the Scalar Matrix kernel..\n");
	ciErrNum = clEnqueueNDRangeKernel(hCmdQueue, ckKernel, 1, NULL, globalWorkSize, NULL, 0, NULL, NULL);
	if(ciErrNum != CL_SUCCESS){
		printf("Error while enqueuing ckKernel: %d\n", ciErrNum);
	}
	clFinish(hCmdQueue); 
	printf("Completed execution..\n");

	ciErrNum = clEnqueueReadBuffer(hCmdQueue, bufferA, CL_TRUE, 0, a * c * sizeof(int), MatrixA, 0, NULL, NULL);
	if(ciErrNum != CL_SUCCESS){
		printf("Error while reading buffers: %d\n", ciErrNum);
	}
	printf("Results received..\n");
	#pragma endregion 

	// release buffers
	clReleaseMemObject(bufferA);
	clReleaseMemObject(bufferB);
	clReleaseMemObject(bufferC);

	cleanup();

	return 0;
}

void cleanup(){

        if(ckKernel)clReleaseKernel(ckKernel);
        if(hProgram)clReleaseProgram(hProgram);
        if(hCmdQueue)clReleaseCommandQueue(hCmdQueue);
        if(hContext)clReleaseContext(hContext);

        // release buffers
		/*clReleaseMemObject(bufferA);
		clReleaseMemObject(bufferB);
		clReleaseMemObject(bufferC);*/

        // free host variables

}

int main(int argc, const char** argv){

        int *matrixA, *matrixB, *matrixC, *matrixCL;
        int m=0,n=0,c=0;
        printf("Run with the default %i x %i x %i configuration?(y/n)\n", M, N, C);
        char answer = getchar();
        if(answer == 'y' || answer == 'y' )
        {
                m = M;
                n = N;
                c = C;
        }
        //else{}

        matrixA = new int[m*c];
        matrixB = new int[c*n];
        matrixC = new int[m*n];
		matrixCL = new int[m*n];

        populateMatrices(matrixA, matrixB, m, n, c);
        multiplyMatrices(matrixA,matrixB, matrixC, m, n, c);

		printf("\n\nA..\n");
        printMatrix(matrixA, m, c);
		printf("\n\nB..\n");
		printMatrix(matrixB, c, n);
		printMatrix(matrixC, m, n);
		
		printf("\n\nStarting OpenCL..\n");

        run_program(matrixA, matrixB, matrixCL, m, n, c);
		printf("\nOpenCL job completed..\n");
		printMatrix(matrixCL, m, n);

		printf("\nOpenCL job completed..");
		printMatrix(matrixA, m, c);

		getchar();

}

void populateMatrices(int *MatrixA, int *MatrixB, int a, int b, int c){
        for(int i = 0; i < a; i++)
        {
                for(int j = 0; j < c; j++)
                {
                        MatrixA[i * c + j] = 1;
                }
        }

        for(int i = 0; i < c; i++)
        {
                for(int j = 0; j < b; j++)
                {
                        MatrixB[i * b + j] = 1;
                }
        }
}

void multiplyMatrices(int *MatrixA, int *MatrixB, int *MatrixC, int a, int b, int c){
        for(int i = 0; i < a; i++)
        {
                for(int j = 0; j < b; j++)
                {
                        MatrixC[i * b + j] = 0;
                        for(int k = 0; k < c; k++)
                        {
                                MatrixC[i * b + j] += (MatrixA[i * c + k] * MatrixB[k * b + j]);
                        }
                }
        }
}

void printMatrix(int *MatrixC, int a, int b){
	printf("\nResult : \n");
	for(int i = 0; i < a; i++)
    {
                printf("\n");
        for(int j = 0; j < b; j++)
        {
                        printf("%i ", MatrixC[i * b + j]);
                }
        }
}